iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0

本文同步發表於個人部落格:FHIR 資源快速入門


一整片用細線串起的病歷卡片鋪滿畫面,中央懸著一塊淡藍色圓形鏡片;鏡片內的同一批卡片被重繪成資料庫的樣子,方正的表格與鎖進插槽的直線連接器,鏡片外則維持柔軟圓潤的卡片與彎曲細線;畫面右上方有一條珊瑚色的線鬆脫懸空,末端散開成細絲,沒有連到任何東西

翻開火線超人的程式碼,有一個服務叫 fhir_patient_service,工作很單純:把病人資料從 FHIR server 抓回來。火線超人後來的許多功能,查掛號、看報告、健康分析,都是從這個最基本的動作長出來的。畢竟要讓病人在 LINE 裡打一句話就看到自己的資料,第一步不是寫聊天機器人,是先搞懂 FHIR 的資料長什麼樣。

昨天說過,你可以把 FHIR server 當成一個醫療專用的資料庫。今天就用資料庫的方式將 FHIR 的三個核心概念先看一遍:Resource、Reference、Bundle。看完我們就可以動手查真的 FHIR 伺服器了。

Resource:標準化的資料表

FHIR 把醫療世界拆成一百多種 Resource,每一種都像一張定義好的資料表:Patient 存病人基本資料、Observation 存檢驗檢查數值、Condition 存診斷、MedicationRequest 存處方。一筆資料就是一份 JSON,長這樣:

{
  "resourceType": "Patient",
  "id": "example",
  "name": [{ "family": "Chen", "given": ["Hsiao-Ming"] }],
  "gender": "male",
  "birthDate": "1990-01-01"
}

每份資料都有兩個欄位:resourceType 說明這是哪張表,id 是這筆資料的主鍵。想拿某一筆資料,URL 也是標準的:

GET [base]/Patient/{id}

跟資料庫不一樣的地方也先說在前面:FHIR 的欄位是巢狀 JSON,而且幾乎每個欄位都可有可無,名字可以有好幾組,性別可以沒填。

Reference:資源之間的外鍵

資料表之間要關聯,資料庫用外鍵,FHIR 用 Reference。一筆檢驗的 Observation 是這樣指回病人的:

{
  "resourceType": "Observation",
  "id": "bp-001",
  "subject": { "reference": "Patient/example" }
}

subject.reference 裡的 Patient/example,就是「這筆檢驗屬於哪位病人」的外鍵。診斷指向病人,處方同時指向病人和開藥的醫師,整份醫療紀錄就是靠 Reference 織成一張網。

真實伺服器上的一筆 Observation 長這樣:

公開 FHIR server 回傳的一筆 Observation,code 區塊是 LOINC 29463-7 Body Weight,subject.reference 指向 Patient/d48ac962-78c6-46cf-ba33-a24771bfa0e4,下方 encounter.reference 另外指向一筆 Encounter,最後 valueQuantity 是 95.15 kg

一筆體重紀錄同時掛著兩條 Reference:subject 指向這是誰的體重,encounter 指向這是哪一次就診量的。

把同一位病人的幾筆資料攤開,那張網長這樣:

以 Patient 為中心的 Reference 關係圖。Observation、Condition、MedicationRequest、Encounter 四個資源各用一條深色箭頭經由 subject 欄位指回中央的 Patient;MedicationRequest 另有一條金色箭頭經由 requester 指向下方的 Practitioner,代表指向病人以外的資源。右側對照表列出資料庫外鍵與 FHIR Reference 的三項差異:形式是欄位加完整性檢查對一行字串、找不到時是寫入被拒對由實作決定、查關聯是 JOIN 對逐一取回;下方標示抽驗 45 條 Reference 全部找得到對應資料

每個箭頭都只是 JSON 裡的一行字串。這是它跟資料庫外鍵最大的差別:沒有 JOIN 可以一次撈完關聯,得逐一取回;資料庫也不會幫你檢查這行字串指向的資料到底存不存在,寫進去的時候擋不擋,完全看伺服器怎麼實作。我在這台測試伺服器上抽驗了 45 條 Reference,每一條都找得到對應的資料。但那是這批測試資料本身乾淨,不是伺服器給你的保證。

Bundle:查詢結果的 Bundle

在一般資料庫下 SQL 的結果通常會是一個資料集,但查 FHIR 拿到的是 Bundle。我們可以對 Patient 做搜尋:

https://r4.smarthealthit.org/Patient?gender=female&_count=5

回來的不是陣列,是一個 type 為 searchset 的 Bundle:entry 裝著每一筆資源,link 裡放著下一頁的網址,total 告訴你符合條件的總共幾筆。

total 這個欄位要特別說一下:它在規格裡是選填的。伺服器算總數要付出代價,所以不少實作只在你給了搜尋條件時才回,沒條件的全表查詢就省略。

兩張圖擺在一起最清楚。先是不給條件的 Patient?_count=3

不給搜尋條件的 Bundle 回應,第 2 行 resourceType 是 Bundle,第 7 行 type 是 searchset,第 8 行直接接 link,裡面有 self 與 next 兩筆,第 18 行才是 entry,整份回應沒有 total 欄位

再來是加了 gender=female 的同一支查詢:

加上搜尋條件後的 Bundle 回應,第 7 行 type 是 searchset 之後,第 8 行多出 total 為 284,link 往下移到第 9 行,entry 移到第 19 行

行號對齊著看:第 7 行都是 type,但第二張的第 8 行多了 total,把後面的 link 和 entry 整個往下擠一行。待會動手時你會親眼看到這個差別,先記著「找不到 total 不代表壞掉」。

搜尋條件就寫在 query string 上,語感跟 WHERE 很接近。

整理成對照表,後端工程師應該會覺得眼熟:

資料庫概念 FHIR 對應
資料表 Resource type(Patient、Observation)
一筆資料 一份 Resource(JSON)
主鍵 id
外鍵 Reference
SELECT 加 WHERE search parameters
JOIN _include(day20 再聊)
查詢結果集 Bundle

先打個預防針:FHIR 是 API 標準,不是儲存引擎,伺服器後面可能接任何資料庫。但對 app 開發者來說,這張對照表已經足夠讓你開工。

如何查 FHIR server

今天用 SMART Health IT 提供的開放測試伺服器,裡面全是假病人資料,不用授權就能查,瀏覽器就是你的查詢工具:

  1. https://r4.smarthealthit.org/Patient?_count=3 ,這是你的第一個 Bundle。找找 resourceType、type、entry、link 四個欄位在哪。順便確認一件事:這次的回應裡沒有 total。
  2. 從 entry 裡隨便挑一筆,記下它的 id,然後開 https://r4.smarthealthit.org/Patient/{id} ,把 {id} 換成剛剛記下的值。這就是「用主鍵撈一筆」。
  3. 加上條件:https://r4.smarthealthit.org/Patient?gender=female&_count=5 。這次 total 出現了,而且它算的是符合條件的總數,不是這一頁的筆數。entry 只有 5 筆,total 卻是幾百。剛才步驟 1 沒有 total,就是因為沒給條件。
  4. 最後看看外鍵:https://r4.smarthealthit.org/Observation?patient={id}&_count=3 ,點開任何一筆,找到 subject.reference,確認它指回你那位病人。

兩個提醒:測試伺服器是公用的,資料可能隨時被清掉,查不到就換一筆;如果整台掛了,備用選手是 hapi.fhir.org/baseR4 ,操作方式一模一樣。

小結

今天用資料庫的眼睛認識了 FHIR 的三個核心:Resource 是標準化的資料表,Reference 是資源之間的外鍵,Bundle 是查詢結果的包裹,而且你已經親手查過真的 FHIR server 了。

最後看一眼規模。同一位病人,把他名下所有資料一次撈出來是這樣:

以病人為中心的資料量圖表。中央黃色圓圈內是病人圖示,右側標示 Observation 有 136 筆為最大宗;下方五張卡片分別是 Procedure 15 筆、DiagnosticReport 11 筆、Immunization 10 筆、MedicationRequest 5 筆、Condition 2 筆;再下方三個數字為 16 種資源型別、68% 集中在一種、4 種指向病人的欄位名;最底部深色區塊列出其餘 10 種資源:Encounter、Appointment、CarePlan、Goal、Communication、Task、ServiceRequest、Device、Practitioner、Organization

一個人就牽動 16 種資源、200 筆資料,而且七成集中在 Observation。更麻煩的是「指向這個人」的欄位名不統一,有 subject、patient、for、actor 四種。

差別在於那個欄位允許指向什麼。subject 最寬鬆,可以是病人、一群受試者,甚至一台裝置或一個地點,所以它不能叫 patient;Immunization 和 Device 用 patient,因為打疫苗、貼裝置的對象一定是人,規格就鎖死了;Task 用 for,指的是這件工作的受益者,做事的人另外記在 owner;Appointment 用 actor,因為一場約診的參與者可能是病人、醫師或診間,病人只是其中一個。

所以你直接讀 JSON 想找「這是誰的資料」時,得先知道每種資源該看哪個欄位。好消息是搜尋參數把這個差異抹平了,十種資源都吃 ?patient={id},這部分 day20 談搜尋的時候再展開。

那如果連「該查哪些資源型別」都不想自己決定呢?FHIR 為此準備了 $everything。它不是搜尋,是一個「操作」,FHIR 用開頭的 $ 跟資源路徑做區分。想自己試就把上面查到的病人 id 換進去:

GET [base]/Patient/{id}/$everything

例如 https://r4.smarthealthit.org/Patient/{id}/$everything?_count=200 。這個操作有個有趣的地方:這台伺服器的 metadata 完全沒有宣告它,也就是說你沒辦法靠探詢知道它存在,只能翻規格或直接試。明天之後我們會發現,伺服器願意告訴你的能力,跟它實際支援的能力常常不是同一回事。

明天把開發環境搭起來:SMART Health IT Launcher 怎麼用、本機專案怎麼起,把 day05 開始拆授權需要的工具一次備齊。


上一篇
Day02 - SMART on FHIR 新地圖
下一篇
Day04 - 開發環境準備
系列文
SMART on FHIR 開發之路:30 天做一個跨醫院的 app4
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言